<?php
// +----------------------------------------------------------------------
// | zhanshop-clouds / Cloudlvs.php    [ 2024/6/29 12:36 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2011~2024 zhangqiquan All rights reserved.
// +----------------------------------------------------------------------
// | Author: Administrator <768617998@qq.com>
// +----------------------------------------------------------------------
declare (strict_types=1);

namespace app\api\admin\v1\service\finder;

use app\api\admin\v1\provider\CloudappProvider;
use extend\docker\Container;
use zhanshop\App;
use zhanshop\client\Httpclient;
use zhanshop\server\Request;
use zhanshop\server\Response;

class Cloudlvs extends BaseFinder
{
    protected $limits = [5000];
    protected $search = false;

    protected $headToolbar = [
        [
            'event' => 'add',
            'ico' => '&#xe608;',
            'title' => '创建',
            'method' => '',
            'page' => './page/docker/create-cloudlvs.html',
        ],
        [
            'event' => 'deletes',
            'ico' => '&#xe640;',
            'title' => '删除',
            'method' => '',
            'page' => '',
        ],
    ];

    protected $rowToolbar = [
        [
            'event' => 'edit',
            'ico' => '&#xe642;',
            'title' => '编辑',
            'method' => '',
            'page' => './page/docker/edit-cloudlvs.html',
        ],
        [
            'event' => 'edit',
            'ico' => '&#xe642;',
            'title' => '证书',
            'method' => '',
            'page' => './page/docker/cert-cloudlvs.html',
        ],
        [
            'event' => 'delete',
            'ico' => '&#xe640;',
            'title' => '删除',
            'method' => '',
            'page' => '',
        ],
    ];

    protected function getCols(string $schma){
        $cols = [
            'name' => array (
                'field' => 'name',
                'title' => '名称',
                'width' => 170,
                'input_type' => 'bool',
                'type' => 'varchar',
                'null' => 'no',
            ),
            'domain' => array (
                'field' => 'domain',
                'title' => '域名',
                'input_type' => 'text',
                'type' => 'varchar',
                'width' => 220,
            ),
            'server_host' => array (
                'field' => 'server_host',
                'title' => '服务地址',
                'width' => 220,
                'input_type' => 'text',
                'type' => 'varchar',
                'null' => 'no',
            ),
            'force_https' => array (
                'field' => 'force_https',
                'title' => '强制https',
                'width' => 120,
                'input_type' => 'bool',
                'type' => 'varchar',
                'null' => 'no',
            ),
        ];
        return $cols;
    }

    public function data(int $page, int $limit, array $order, array $search)
    {
        $confPath = App::rootPath().'/docker/nginx/conf/';
        $files = glob($confPath.'*.conf');

        $list = [];
        foreach ($files as $k => $v){
            $confData = file_get_contents($v);
            $fileName = pathinfo($v, PATHINFO_FILENAME);
            $host = $fileName;
            $proxy = '';
            if(strpos($confData, 'proxy_pass ') !== false) {
                $proxy = str_replace(' ', '', explode(';', explode("proxy_pass", $confData)[1])[0]);
            }elseif(strpos($confData, 'root /var/www/') !== false){
                $proxy = str_replace(' ', '', explode(';', explode("root /var/www/", $confData)[1])[0]);
            }
            $name = $host;
            $annotations = explode("\n", $confData, 2)[0];
            if(strpos($annotations, '#') === 0) $name = substr($annotations, 1);

            $list[] = [
                'domain' => $host,
                'server_host' => $proxy,
                'name' => $name,
                'force_https' => (strpos($confData, '301 https://') === false ? false : true),
            ];
        }

        return [
            'total' => count($list),
            'list' => $list
        ];
    }

    public function post(Request $request, Response $response){
        $post = $request->post();
        if(strpos($post['domain'], ' ') !== false){
            App::error()->setError($post['domain'].'中不能存在空格', 403);
        }
        $maxBody = ceil(floatval($post['max_body'] ?? 1));
        $proxyAuth = App::config()->get("app.rproxy_auth", 'zhanshop');
        if($maxBody <= 0) $maxBody = 1;
        $confPath = App::rootPath().'/docker/nginx/conf/'.(explode(' ', $post['domain'])[0]).'.conf';
        if(file_exists($confPath) == true){
            App::error()->setError($confPath.'已经存在', 403);
        }

        $post['service_host'] = rtrim($post['service_host'], '/');

        $forceHttps = $post['force_https'] ?? 0;

        if(strpos($post['service_host'], 'http') === 0){
            $location = 'proxy_pass '.$post['service_host'].'/;';
            self::proxyHostCheck($post['service_host']);
        }else{
            $localDir = App::rootPath().'/docker/nginx/html/'.$post['service_host'];
            if(!file_exists($localDir)){
                App::error()->setError($localDir.'是一个不存在的文件或者目录', 404);
            }
            $location = "root /var/www/".$post['service_host'].";  # 指定www目录
        index index.html index.htm;
        autoindex on; # 禁止列出目录
        error_log /dev/stderr notice;
        log_not_found off;";
        }


        if($forceHttps){
            $httpLocation = 'return 301 https://$host$request_uri;';
        }else{
            $httpLocation = $location;
        }

        $confStr = '#'.($post['name'] ?? 'unknown').'
server {
    listen       80;
    listen       [::]:80;
    listen       [::]:8000;
    server_name  '.$post['domain'].';
    
    #限制请求大小
    client_max_body_size '.$maxBody.'M;
    #代理配置参数
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header proxy-auth '.$proxyAuth.';
    location / {
        '.$httpLocation.'
    }
}';

        if($post['certificate'] && $post['certificate']){
            $crtFileName = (explode(' ', $post['domain'])[0]).'.crt';
            $keyFileName = (explode(' ', $post['domain'])[0]).'.key';
            file_put_contents(App::rootPath().'/docker/nginx/ssl/'.$crtFileName, $post['certificate']);
            file_put_contents(App::rootPath().'/docker/nginx/ssl/'.$keyFileName, $post['private_key']);
            $confStr .= '
# 优化SSL配置
ssl_early_data on;
ssl_session_tickets on;
server {
    #SSL 默认访问端口号为 443
    listen 443 ssl;
    listen [::]:443 ssl;
    listen 443 quic reuseport;
    listen [::]:443 quic reuseport;
    http2 on;
    #请填写绑定证书的域名
    server_name '.$post['domain'].';  #注意填写自己的域名
    #请填写证书文件的相对路径或绝对路径
    ssl_certificate /etc/nginx/ssl/'.$crtFileName.';   #步骤2中拷贝的证书文件
    #请填写私钥文件的相对路径或绝对路径
    ssl_certificate_key /etc/nginx/ssl/'.$keyFileName.';    #步骤2中拷贝的私钥文件.
    ssl_session_timeout 5m;
    #请按照以下协议配置
    ssl_protocols TLSv1.2 TLSv1.3;
    #请按照以下套件配置，配置加密套件，写法遵循 openssl 标准。
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
    ssl_prefer_server_ciphers on;
    
    #限制请求大小
    client_max_body_size '.$maxBody.'M;
    #代理配置参数
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header proxy-auth '.$proxyAuth.';
    add_header Alt-Svc \'h3=":443"; ma=86400\' always;
    location / {
        '.$location.'
    }
}
';
        }

        file_put_contents($confPath, $confStr);
        try {
            self::restartNginx(); // 重启nginx
        }catch (\Throwable $error){
            @unlink($confPath);
            App::error()->setError($error->getMessage(), 400);
        }
    }

    public function editfrom(Request $request, Response $response)
    {
        $pk = $request->get('pk');
        $pk = explode(' ', $pk)[0];
        $confPath = App::rootPath().'/docker/nginx';
        $confData = file_get_contents($confPath.'/conf/'.$pk.'.conf');

        return [
            'domain' => $pk,
            'config' => $confData
        ];
    }

    public function put(Request $request, Response $response)
    {
        $domain = $request->post('old_domain');
        $config = $request->post('conf');

        $oldConf = App::rootPath().'/docker/nginx/conf/'.$domain.'.conf';
        if(file_exists($oldConf)){
            copy($oldConf, $oldConf.'.back'); // 复制一份老的
        }
        file_put_contents($oldConf, $config);
        try {
            self::restartNginx(); // 重启nginx
            @unlink($oldConf.'.back');
        }catch (\Throwable $error){
            // 还原
            rename($oldConf.'.back', $oldConf);
            App::error()->setError($error->getMessage(), 400);
        }
    }

    public function certfrom(Request $request, Response $response)
    {
        $domain = $request->get('pk');
        $crtFile = App::rootPath().'/docker/nginx/ssl/'.$domain.'.crt';
        $keyFile = App::rootPath().'/docker/nginx/ssl/'.$domain.'.key';
        if(file_exists($crtFile) == false || file_exists($keyFile) == false){
            App::error()->setError($domain.'暂不支持配置证书,请删除后重建带证书的配置');
        }
        return [
            'domain' => $domain,
            'certificate' => file_get_contents($crtFile),
            'private_key' => file_get_contents($keyFile)
        ];
    }

    public function savecert(Request $request, Response $response)
    {
        $domain = $request->post('domain');
        $certificate = $request->post('certificate');
        $privateKey = $request->post('private_key');
        $crtFile = App::rootPath().'/docker/nginx/ssl/'.$domain.'.crt';
        $keyFile = App::rootPath().'/docker/nginx/ssl/'.$domain.'.key';
        if(file_exists($crtFile) == false || file_exists($keyFile) == false){
            App::error()->setError($domain.'证书文件未被初始化');
        }
        file_put_contents($crtFile, $certificate);
        file_put_contents($keyFile, $privateKey);
        self::restartNginx(); // 重启nginx
    }

    public function delete(Request $request, Response $response)
    {
        $input = $request->post();
        foreach($input['pk'] as $domain){
            $confPath = App::rootPath().'/docker/nginx/conf/'.$domain.'.conf';
            @unlink($confPath);

            $crtFile = App::rootPath().'/docker/nginx/ssl/'.$domain.'.crt';
            $keyFile = App::rootPath().'/docker/nginx/ssl/'.$domain.'.key';
            if(file_exists($crtFile)) unlink($crtFile);
            if(file_exists($keyFile)) unlink($keyFile);
        }
        self::restartNginx(); // 重启nginx
    }

    /**
     * 重启nginx
     * @return void
     */
    public static function restartNginx()
    {
        $apps = App::make(CloudappProvider::class)->getList();
        $nginxId = "";
        foreach($apps['list'] as $app){
            if($app['service_name'] == 'zhanshop-nginx'){
                $nginxId = $app['id'];
                break;
            }
        }
        if($nginxId){
            $result = (new Container())->exec($nginxId, 'nginx -t', [
                "AttachStdin" => false,
                "AttachStdout" => true,
                "AttachStderr" => true,
                "Tty" => false,
                "DetachKeys" => "ctrl-p,q",
            ]);
            if($result != false && is_string($result)){
                App::error()->setError("nginx检查有错误，请检查输入参数的正确性");
            }
        }
    }

    /**
     * 代理地址检查
     * @param string $host
     * @return void
     */
    public static function proxyHostCheck(string $host)
    {
        $resp = (new Httpclient())->setTimeout(500)->request($host);
        if($resp['code'] < 0){
            App::error()->setError($host.'地址无效或无法访问', 403);
        }
    }

    /**
     * 删除代理
     * @param string $host
     * @return void
     */
    public static function proxyHostDelete(string $host)
    {
        $files = glob(App::rootPath().'/docker/nginx/conf/*.conf');
        foreach($files as $v){
            $confData = file_get_contents($v);
            if(strpos($confData, 'proxy_pass http://'.$host.':') !== false){
                @unlink($v);
                break;
            }
        }
    }
}